Skip to content

[Feature] 개인별 그룹 색상 할당 기능 구현#87

Merged
dioo1461 merged 9 commits intodevfrom
feature/group-color-management
Feb 6, 2026
Merged

[Feature] 개인별 그룹 색상 할당 기능 구현#87
dioo1461 merged 9 commits intodevfrom
feature/group-color-management

Conversation

@dioo1461
Copy link
Collaborator

@dioo1461 dioo1461 commented Feb 4, 2026

  • close #

✨ 구현 기능 명세

  • 처음 접하는 그룹에 랜덤한 색상이 할당되도록 구현했습니다.
image
  • 그룹 카테고리는 속한 그룹의 색상을 띠도록 구현했습니다.
    • 그룹에 속하지 않은 일반 카테고리는 노란색으로 표시됩니다.
image
  • 그룹 색상 변경 기능 구현했습니다.
image

✅ PR Point

😭 어려웠던 점

@dioo1461 dioo1461 requested a review from Copilot February 4, 2026 09:28
@dioo1461 dioo1461 self-assigned this Feb 4, 2026
@dioo1461 dioo1461 added the ✨ feature 새로운 기능 구현 label Feb 4, 2026
@gemini-code-assist
Copy link

Summary of Changes

Hello @dioo1461, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 풀리퀘스트는 사용자별 그룹 색상 할당 기능을 도입하여 그룹 관리의 개인화를 강화합니다. 이를 위해 새로운 React 훅을 추가하고, 기존 그룹 관련 컴포넌트들을 리팩토링했습니다. 또한, 학습 시간 기록 로직의 안정성을 개선하고, 비동기 저장소 키 관리 방식을 업데이트하여 데이터의 정확성과 사용자 경험을 향상시켰습니다.

Highlights

  • 개인별 그룹 색상 할당 기능 구현: 사용자별로 그룹에 무작위 색상을 할당하고, 이 색상을 asyncStorage에 저장하여 영속성을 확보했습니다. 이 색상은 그룹 목록, 그룹 상세, 그룹 랭킹, 그룹 설정 화면 등 여러 그룹 관련 화면에서 사용됩니다.
  • 그룹 목록 UI 리팩토링: JoinedGroupPanel 컴포넌트 내에서 그룹 목록을 렌더링하는 로직을 GroupListItem이라는 별도의 컴포넌트로 분리하여 가독성과 재사용성을 높였습니다.
  • 학습 시간 기록 로직 개선: StudyDataHydratoruseStartStudyMutation에서 학습 기록이 없을 경우 null 대신 Time.fromMilliseconds(0)을 반환하도록 수정하여 잠재적인 런타임 오류를 방지했습니다.
  • 비동기 저장소 키 업데이트: 그룹 채팅 커서(groupChatCursor) 키에 userId를 포함하도록 변경하여 여러 사용자가 동일한 기기를 사용할 때 발생할 수 있는 데이터 충돌 가능성을 줄였습니다. 또한, 그룹 색상 저장을 위한 새로운 키(groupColor)를 추가했습니다.
  • UI/UX 개선: StackableScreenLayoutsafeAreas 기본값을 'top'으로 설정하고, 그룹 관련 화면(랭킹, 설정, 상세)의 헤더 요소 색상을 그룹 색상에 맞춰 더 강한 톤으로 변경하여 시각적 일관성을 높였습니다.
Changelog
  • src/StudyDataHydrator.tsx
    • todayRecordnull일 경우 Time.fromMilliseconds(0)을 사용하도록 변경하여 오류 처리 로직을 간소화했습니다.
  • src/features/groups/hooks/useGroupColorKey.ts
    • 새로운 훅 useGroupColorKey를 추가하여 특정 그룹에 대한 사용자별 색상을 할당하고 asyncStorage에 저장 및 조회하는 로직을 구현했습니다.
  • src/features/study/api/mutations.ts
    • todayRecordnull일 경우 Time.fromMilliseconds(0)을 사용하도록 변경하여 학습 시간 계산의 안정성을 높였습니다.
    • StudyDataHydrator와 중복되는 로직 리팩토링에 대한 TODO 주석을 추가했습니다.
  • src/layout/StackableScreenLayout/index.tsx
    • safeAreas prop의 기본값을 'top'으로 설정했습니다.
  • src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx
    • 그룹 목록 렌더링 로직을 GroupListItem 컴포넌트로 분리하고, useGroupColorKey 훅을 사용하여 그룹별 색상을 동적으로 할당하도록 변경했습니다.
  • src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts
    • asyncStoragegroupChatCursor 키에 myUserId를 포함하도록 수정했습니다.
  • src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts
    • myUserId의 타입을 number | null에서 number로 변경하고, asyncStoragegroupChatCursor 키에 myUserId를 포함하도록 수정했습니다.
  • src/screens/group/GroupChatScreen/index.tsx
    • useChatDBuseGroupChatRealtime 훅에 전달되는 userIdmyUserId의 기본값을 null에서 -1로 변경했습니다.
  • src/screens/group/GroupRankingScreen/index.tsx
    • StackableScreenLayoutheaderElementColor를 그룹 색상의 'medium'에서 'strong'으로 변경했습니다.
  • src/screens/group/GroupSettingScreen/index.tsx
    • StackableScreenLayoutheaderElementColor를 그룹 색상의 'medium'에서 'strong'으로 변경했습니다.
  • src/screens/group/groupDetail/GroupDetailScreen/index.tsx
    • useRoute 훅을 임포트하고, groupColoruseStackRoute에서 가져온 groupColorKey를 기반으로 동적으로 설정하도록 변경했습니다.
    • StackableScreenLayoutbackgroundColorheaderElementColor를 그룹 색상에 맞춰 동적으로 적용했습니다.
  • src/utils/Time/index.ts
    • 시간 차이가 음수일 경우 console.warn 대신 console.log를 사용하도록 변경했습니다.
  • src/utils/asyncStorage/index.ts
    • ASYNC_KEYS.groupChatCursor 함수에 userId 파라미터를 추가하여 키 생성 방식을 변경했습니다.
    • 그룹 색상 저장을 위한 새로운 키 ASYNC_KEYS.groupColor를 추가했습니다.
Activity
  • 이 풀리퀘스트에는 아직 인간 활동(댓글, 리뷰 등)이 없습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이 PR은 그룹별 색상 할당 기능을 도입하여 사용자 경험을 크게 향상시켰습니다. 구현은 "asyncStorage"를 사용하여 사용자 및 그룹별로 색상 할당을 올바르게 유지합니다. 또한, 채팅 커서에 대한 사용자별 데이터 저장과 관련하여 "asyncStorage"에 몇 가지 중요한 수정 사항이 적용되어 데이터 격리 및 정확성이 크게 개선되었습니다. 그룹 목록 렌더링을 별도의 컴포넌트로 리팩토링한 것도 유지보수성을 높였습니다.

await storeChats(entities);
const recentChatId = fetchedChats[fetchedChats.length - 1].chatId;
asyncStorage.set(ASYNC_KEYS.groupChatCursor(chatRoomId), recentChatId);
asyncStorage.set(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId), recentChatId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

"groupChatCursor" 키에 "myUserId"를 포함하도록 변경한 것은 매우 중요합니다. 이는 각 사용자의 채팅 커서가 독립적으로 저장되도록 보장하여, 여러 사용자가 동일한 기기에서 앱을 사용하거나 사용자 계정이 전환될 때 발생할 수 있는 데이터 충돌을 방지하는 치명적인 수정입니다.

Comment on lines +17 to +18
groupChatCursor: (userId: number, chatRoomId: string) => `groupChat:${userId}-${chatRoomId}`,
groupColor: (userId: number, groupId: number) => `groupColor:${userId}-${groupId}`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

"groupChatCursor" 키에 "userId"를 포함하도록 수정한 것은 채팅 커서에 대한 데이터 무결성을 보장하는 치명적인 개선입니다. 또한, "groupColor" 키를 "userId"와 "groupId"를 모두 포함하도록 추가한 것은 새로운 그룹 색상 할당 기능을 올바르게 저장하고 검색하는 데 필수적입니다.


await storeChats([chatObjToEntity(newChat, chatRoomId)]);
await asyncStorage.set(ASYNC_KEYS.groupChatCursor(chatRoomId), message.id);
await asyncStorage.set(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId), message.id);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

"groupChatCursor" 키에 "myUserId"를 포함하도록 변경한 것은 "useGroupChatBootstrap.ts"와 동일하게 사용자별 데이터 격리를 위한 치명적인 수정입니다.


const cursor =
(await asyncStorage.get(ASYNC_KEYS.groupChatCursor(chatRoomId))) ?? undefined;
(await asyncStorage.get(ASYNC_KEYS.groupChatCursor(myUserId, chatRoomId))) ?? undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

"groupChatCursor" 키에 "myUserId"를 포함하도록 변경한 것은 매우 중요합니다. 이는 각 사용자의 채팅 커서가 독립적으로 저장되도록 보장하여, 여러 사용자가 동일한 기기에서 앱을 사용하거나 사용자 계정이 전환될 때 발생할 수 있는 데이터 충돌을 방지하는 치명적인 수정입니다.


const GroupDetailScreen = () => {
const groupColor = GROUP_COLORS[theme.color.blue[300]];
const groupColor = GROUP_COLORS[useStackRoute<'GroupDetail'>().params.groupColorKey];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

"groupColor"를 하드코딩된 파란색 대신 "useStackRoute().params.groupColorKey"에서 동적으로 가져오도록 변경한 것은 그룹별 색상 할당 기능의 핵심적인 부분입니다. 이는 그룹 상세 화면이 할당된 그룹 색상을 올바르게 반영하도록 보장합니다.

<StackableScreenLayout
backgroundColor={GROUP_COLORS[groupColorKey].weak}
headerElementColor={GROUP_COLORS[groupColorKey].medium}
headerElementColor={GROUP_COLORS[groupColorKey].strong}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

"headerElementColor"를 "GROUP_COLORS[groupColorKey].strong"으로 변경하여 그룹 색상 테마의 'strong' 변형을 사용하도록 한 것은 UI의 시각적 일관성을 높입니다.

header,
children,
safeAreas,
safeAreas = 'top',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

"safeAreas" prop에 "top"을 기본값으로 설정하여 컴포넌트 사용 시 명시적으로 지정하지 않아도 안전 영역 처리가 되도록 개선했습니다. 이는 컴포넌트의 사용 편의성을 높입니다.

Comment on lines +60 to +61
backgroundColor={groupColor.weak}
headerElementColor={groupColor.strong}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

하드코딩된 색상 대신 "groupColor.weak" 및 "groupColor.strong"을 사용하여 배경 및 헤더 요소 색상을 동적으로 설정한 것은 그룹별 색상 할당 기능과의 일관성을 높이는 좋은 변경입니다.

if (diff < 0) {
// eslint-disable-next-line no-console
console.warn('Resulting time is negative. Returning zero time.');
console.log('[Time] Resulting time is negative. Returning zero time.');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

음수 시간 결과에 대한 로깅을 "console.warn"에서 "console.log"로 변경한 것은 해당 상황이 경고 수준의 문제가 아니라 정보성 메시지임을 나타냅니다. 이는 로깅의 적절성을 개선합니다.

}

const recordedStudyTime = todayRecord.time;
const recordedStudyTime = todayRecord?.time ?? Time.fromMilliseconds(0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

"StudyDataHydrator.tsx"와 마찬가지로 "todayRecord"가 "null"일 때 "console.error" 대신 "?? Time.fromMilliseconds(0)"를 사용하여 기본값을 할당하는 것은 일관성 있고 안정적인 오류 처리 방식입니다.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ecdb44ee2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 50 to 54
loadMoreChats,
storeChats,
setChats,
myUserId: profileData?.id ?? null,
myUserId: profileData?.id ?? -1,
sendReadCursor,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid bootstrapping chats with placeholder user id

Using profileData?.id ?? -1 means useGroupChatBootstrap runs before the real profile id is loaded. In that first pass it fetches and appends chats, and stores the cursor under groupChat:-1-…. When the profile data arrives, the effect re-runs with the real id, reads an empty cursor (different key) and fetches the same history again, appending duplicates to chats. This happens any time the profile query resolves after the chat screen mounts. Consider keeping myUserId as null until the profile is ready, or guard the bootstrap hook so it only runs when a valid user id is available.

Useful? React with 👍 / 👎.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a feature to assign random colors to groups on a per-user basis, ensuring each user sees groups with consistent but personalized colors.

Changes:

  • Added useGroupColorKey hook that stores and retrieves user-specific group colors from async storage
  • Updated async storage keys to include user ID for per-user data isolation
  • Changed chat-related hooks to use -1 instead of null as a fallback for missing user IDs
  • Refactored error handling in study time tracking to use default values instead of logging errors

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/features/groups/hooks/useGroupColorKey.ts New hook that generates and persists random group colors per user
src/utils/asyncStorage/index.ts Added user ID to storage keys for groupChatCursor and groupColor
src/screens/bottomTab/GroupsScreen/components/JoinedGroupPanel/index.tsx Refactored to use new useGroupColorKey hook with GroupListItem component
src/screens/group/groupDetail/GroupDetailScreen/index.tsx Updated to use groupColorKey from route params and apply colors to layout
src/screens/group/GroupSettingScreen/index.tsx Changed header element color from medium to strong
src/screens/group/GroupRankingScreen/index.tsx Changed header element color from medium to strong
src/screens/group/GroupChatScreen/index.tsx Changed userId fallback from null to -1
src/screens/group/GroupChatScreen/hooks/useGroupChatRealtime.ts Updated myUserId type from number | null to number
src/screens/group/GroupChatScreen/hooks/useGroupChatBootstrap.ts Updated to use new groupChatCursor key with userId parameter
src/features/study/api/mutations.ts Removed error logging and used default Time value when record is missing
src/StudyDataHydrator.tsx Removed error logging and used default Time value when record is missing
src/layout/StackableScreenLayout/index.tsx Added default value 'top' for safeAreas prop
src/utils/Time/index.ts Changed console.warn to console.log for negative time warning

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 24 to 32
(async () => {
const stored = await asyncStorage.get(ASYNC_KEYS.groupColor(userId, groupId));
if (stored && isGroupColorKey(stored)) {
if (!cancelled) setColorKey(stored);
return;
}
const newKey = pickRandomGroupColorKey();
await asyncStorage.set(ASYNC_KEYS.groupColor(userId, groupId), newKey);
if (!cancelled) setColorKey(newKey);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup function sets cancelled = true after the async operation has started, but there's a potential race condition. If the component unmounts while the async operation is in progress (between lines 25-32), setColorKey could still be called after the component has unmounted if the check if (!cancelled) hasn't been reached yet. The cancellation flag should be checked before each state update, but line 32 only checks it once. Consider checking cancelled before line 27 as well to prevent state updates on unmounted components.

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 42
const GroupListItem = ({ group }: { group: GetGroupsResponse['data'][number] }) => {
const groupColorKey = useGroupColorKey(group.groupId);
if (!groupColorKey) return null;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook returns null when groupColorKey is null, causing the GroupListItem component to return null and not render. This creates an inconsistent user experience where groups may temporarily not appear in the list while their color is being loaded from async storage. Consider providing a default color or loading state instead, or ensure that the color is loaded before rendering the group list.

Copilot uses AI. Check for mistakes.
Comment on lines 19 to 22
const groupColor = GROUP_COLORS[useStackRoute<'GroupDetail'>().params.groupColorKey];

const { data: categories } = useCategoriesQuery();
const { params } = useStackRoute<'GroupDetail'>();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useStackRoute hook is called twice on lines 19 and 22, which is redundant and violates the React principle of calling hooks in a consistent order. The first call on line 19 is only used to access params.groupColorKey, while the second call is destructured to get params. Consider consolidating these into a single call by storing the result in a variable and accessing both properties from it.

Suggested change
const groupColor = GROUP_COLORS[useStackRoute<'GroupDetail'>().params.groupColorKey];
const { data: categories } = useCategoriesQuery();
const { params } = useStackRoute<'GroupDetail'>();
const { params } = useStackRoute<'GroupDetail'>();
const groupColor = GROUP_COLORS[params.groupColorKey];
const { data: categories } = useCategoriesQuery();

Copilot uses AI. Check for mistakes.
@@ -1,4 +1,4 @@
import { useFocusEffect } from '@react-navigation/native';
import { useFocusEffect, useRoute } from '@react-navigation/native';
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useRoute import is unused after the refactoring. This import should be removed.

Suggested change
import { useFocusEffect, useRoute } from '@react-navigation/native';
import { useFocusEffect } from '@react-navigation/native';

Copilot uses AI. Check for mistakes.
isConnected: boolean;
chatRoomId: string;
myUserId: number | null;
myUserId: number;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of myUserId was changed from number | null to number. However, this change may cause type inconsistencies with other parts of the system. The useChatDB hook still expects userId: number | null, and the calling code in GroupChatScreen passes profileData?.id ?? -1 as a fallback. It appears -1 is being used as a sentinel value for missing user IDs, but this is not documented. Consider using a consistent approach - either update all related types to use -1 as a sentinel value with proper documentation, or keep using null to represent missing user IDs.

Suggested change
myUserId: number;
myUserId: number | null;

Copilot uses AI. Check for mistakes.
export const ASYNC_KEYS = {
language: 'language',
groupChatCursor: (chatRoomId: string) => `groupChat:${chatRoomId}`,
groupChatCursor: (userId: number, chatRoomId: string) => `groupChat:${userId}-${chatRoomId}`,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature of groupChatCursor was changed to include userId as the first parameter, but existing stored keys from the previous version will not be accessible anymore (keys were previously groupChat:${chatRoomId}, now groupChat:${userId}-${chatRoomId}). This is a breaking change that will lose chat cursor data for existing users. Consider implementing a migration strategy or documenting this as an intentional data reset.

Copilot uses AI. Check for mistakes.
if (diff < 0) {
// eslint-disable-next-line no-console
console.warn('Resulting time is negative. Returning zero time.');
console.log('[Time] Resulting time is negative. Returning zero time.');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing from console.warn to console.log reduces the visibility of this edge case (negative time differences). In the codebase, console.warn is consistently used for warning scenarios (see src/features/chat/sse/messageParsers.ts:23, src/features/chat/sse/messageParsers.ts:36, src/features/chat/sse/messageParsers.ts:45, src/hooks/useStompConnection/index.ts:15, src/utils/fetch/executeFetch.ts:62). Consider keeping console.warn for consistency and better visibility of this edge case.

Copilot uses AI. Check for mistakes.
@dioo1461 dioo1461 force-pushed the feature/group-color-management branch from 5ecdb44 to 5bb0b2d Compare February 4, 2026 10:33
@dioo1461 dioo1461 merged commit 4dffcc5 into dev Feb 6, 2026
1 check passed
@dioo1461 dioo1461 deleted the feature/group-color-management branch February 6, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 구현

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants